package dryven.model.binding.proxy;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * This class represents a delayed method call.
 * @author bwindels
 *
 * @param <T> The return type of the method call.
 */
public class Binding<T> {
	
	private Method _boundMethod;
	private Object[] _boundParams;
	
	public Binding(BindingProxy proxy, T boundValue) {
		_boundMethod = proxy.getCalledMethod();
		_boundParams = proxy.getCalledParams();
	}
	
	public Binding(Method m) {
		_boundMethod = m;
	}
	
	public Binding(String encodedMethod) {
		String[] parts = encodedMethod.split(":");
		if(parts.length<2) {
			throw new RuntimeException("invalid encoded method");
		}
		String typeName = parts[0];
		String methodName = parts[1];
		String methodParamTypesNames[] = {};
		if(parts.length==3 && parts[2].length()!=0) {
			methodParamTypesNames = parts[2].split(",");
		}
		try {
			Class<?> type = Class.forName(typeName);
			Class<?> paramTypes[] = new Class<?>[methodParamTypesNames.length];
			for(int i=0;i<paramTypes.length;++i) {
				String paramTypeName = methodParamTypesNames[i];
				paramTypes[i] = getClassForTypeName(paramTypeName);
			}
			Method m = type.getMethod(methodName, paramTypes);
			_boundMethod = m;
		} catch (Exception e) {
			throw new RuntimeException("encoded method does not exist", e);
		}
	}
	
	private Class<?> getClassForTypeName(String type) {
		if(type.equals(Integer.TYPE.getName())) {
			return Integer.TYPE;
		} else if(type.equals(Boolean.TYPE.getName())) {
			return Boolean.TYPE;
		} else if(type.equals(Short.TYPE.getName())) {
			return Short.TYPE;
		} else if(type.equals(Byte.TYPE.getName())) {
			return Byte.TYPE;
		} else if(type.equals(Double.TYPE.getName())) {
			return Double.TYPE;
		} else if(type.equals(Float.TYPE.getName())) {
			return Float.TYPE;
		} else if(type.equals(Long.TYPE.getName())) {
			return Long.TYPE;
		} else {
			try {
				return Class.forName(type);
			} catch (ClassNotFoundException e) {
				throw new RuntimeException(e);
			}
		}
	}
	
	public Method getBoundMethod() {
		return _boundMethod;
	}
	
	/**
	 * Only returns something in case the Binding was constructed via a BindingProxy.
	 * In case one of the other constructors is used, this method returns null.
	 * 
	 * @return The actual values with which the proxy method was called
	 */
	public Object[] getBoundParams() {
		return _boundParams;
	}

	@Override
	public String toString() {
		StringBuffer buf = new StringBuffer();
		for (Class<?> type : getBoundMethod().getParameterTypes()) {
			buf.append(type.getName());
			buf.append(",");
		}
		if(buf.length()!=0) {
			buf.deleteCharAt(buf.length()-1);
		}
		buf.insert(0,getBoundMethod().getDeclaringClass().getName()+":"+getBoundMethod().getName()+":");
		return buf.toString();
	}
	
	@SuppressWarnings("unchecked")
	public T invoke() {
		Object o = null;
		try {
			Constructor<?> ctor = _boundMethod.getDeclaringClass().getDeclaredConstructor(new Class<?>[]{});
			ctor.setAccessible(true);
			try {
				o = ctor.newInstance(new Object[]{});
			} finally {
				ctor.setAccessible(false);
			}
		} catch (Exception e) {
			throw new RuntimeException("Could not invoke default constructor on "+_boundMethod.getDeclaringClass().getName());
		}
		try {
			Object[] params = new Object[_boundMethod.getParameterTypes().length];
			if(_boundParams!=null) {
				for(int i=0;i<_boundParams.length && i<params.length;++i) {
					params[i] = _boundParams[i];
				}
			}
			return (T)_boundMethod.invoke(o, params);
		} catch (InvocationTargetException e) {
			throw new RuntimeException(e);
		} catch (Exception e) {
			throw new RuntimeException("Could not invoke bound method",e);
		}
	}
	
	
}